/*-
 *******************************************************************************
 * Copyright (c) 2015 Diamond Light Source Ltd.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Peter Chang - initial API and implementation and/or initial documentation
 *******************************************************************************/

package org.eclipse.dawnsci.nexus.impl;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import org.eclipse.dawnsci.analysis.api.tree.Attribute;
import org.eclipse.dawnsci.analysis.api.tree.DataNode;
import org.eclipse.dawnsci.analysis.api.tree.GroupNode;
import org.eclipse.dawnsci.analysis.api.tree.Node;
import org.eclipse.dawnsci.analysis.api.tree.NodeLink;
import org.eclipse.dawnsci.analysis.api.tree.SymbolicNode;
import org.eclipse.dawnsci.analysis.tree.TreeFactory;
import org.eclipse.dawnsci.analysis.tree.impl.GroupNodeImpl;
import org.eclipse.dawnsci.nexus.NXobject;
import org.eclipse.dawnsci.nexus.NexusNodeFactory;
import org.eclipse.january.DatasetException;
import org.eclipse.january.dataset.DTypeUtils;
import org.eclipse.january.dataset.Dataset;
import org.eclipse.january.dataset.DatasetFactory;
import org.eclipse.january.dataset.DatasetUtils;
import org.eclipse.january.dataset.DateDataset;
import org.eclipse.january.dataset.IDataset;
import org.eclipse.january.dataset.ILazyDataset;
import org.eclipse.january.dataset.ILazyWriteableDataset;
import org.eclipse.january.dataset.LazyWriteableDataset;
import org.eclipse.january.dataset.StringDataset;

/**
 * The abstract superclass of all base class implementation classes.
 * Unlike the base class implementation classes, this class is
 * <em>not</em> autogenerated.
 */
public abstract class NXobjectImpl extends GroupNodeImpl implements NXobject {

	protected static final long serialVersionUID = GroupNodeImpl.serialVersionUID;

	/**
	 * Name of attribute
	 */
	public static final String NX_CLASS = "NX_class";
	
	private static final int CACHE_LIMIT = 1024;

	private Map<String, Dataset> cached = new HashMap<>();

	/**
	 * Creates a new NeXus group node. This constructor is used when
	 * create a new NeXus file
	 */
	protected NXobjectImpl() {
		super(NexusNodeFactory.getNextOid());
		createNxClassAttribute();
	}
	
	/**
	 * Creates a new NeXus group node. This constructor is used when loading
	 * a new NeXus file. No further nodes should be added to a NeXus tree that has
	 * been loaded from disk.
	 * @param oid
	 */
	protected NXobjectImpl(long oid) {
		super(oid);
		createNxClassAttribute();
	}

	private Dataset getCached(String name) {
		if (!cached.containsKey(name)) {
			DataNode dataNode = getDataNode(name);
			if (dataNode != null) {
				ILazyDataset lazy = dataNode.getDataset();
				if (!(lazy instanceof IDataset)) {
					// if this is a lazy dataset, set the slice on it
					int size = lazy.getSize();
					if (size > CACHE_LIMIT) {
						// cannot return a Dataset if the size is too large
						throw new IllegalStateException("Dataset is too large to cache. This method should only be used for small datasets.");
					} else {
						try {
							lazy = lazy.getSlice();
						} catch (DatasetException e) {
							throw new RuntimeException("Could not get data from lazy dataset", e);
						}
					}
				}
				cached.put(name, DatasetUtils.convertToDataset((IDataset) lazy));
			}
		}
		return cached.get(name);
	}

	@Override
	public boolean canAddChild(NXobject nexusObject) {
		return getPermittedChildGroupClasses().contains(nexusObject.getNexusBaseClass());
	}

	private void createNxClassAttribute() {
		Attribute a = TreeFactory.createAttribute(NX_CLASS);
		String n = getNXclass().getName();
		int i = n.lastIndexOf(".");
		a.setValue(n.substring(i + 1));
		addAttribute(a);
	}

	@SuppressWarnings("unchecked")
	@Override
	public <N extends NXobject> N getChild(String name, Class<N> nxClass) {
		GroupNode g = getGroupNode(name);
		if (g != null && g instanceof NXobject && ((NXobject) g).getNXclass().equals(nxClass)) {
			return (N) g;
		}
		
		return null;
	}
	
	@Override
	public IDataset getDataset(String name) {
		if (!containsDataNode(name)) {
			return null;
		}
		return getCached(name);
	}
	
	@Override
	public ILazyWriteableDataset getLazyWritableDataset(String name) {
		if (containsDataNode(name)) {
			ILazyDataset dataset = getDataNode(name).getDataset();
			if (dataset instanceof ILazyWriteableDataset) {
				return (ILazyWriteableDataset) dataset;
			}
		}
		
		return null;
	}

	@Override
	public DataNode setDataset(String name, IDataset value) {
		DataNode dataNode;
		if (containsDataNode(name)) {
			dataNode = getDataNode(name);
			dataNode.setDataset(value);
		} else {
			dataNode = createDataNode(name, value);
		}
		// update the cache
		if (value instanceof Dataset) {
			cached.put(name, (Dataset) value);
		} else {
			// if this is a lazy dataset only, only clear the old value
			// the new value will be calculated when required
			cached.remove(name);
		}
		
		return dataNode;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.dawnsci.nexus.NXobject#initializeLazyDataset(java.lang.String, int, int)
	 */
	public ILazyWriteableDataset initializeLazyDataset(String name, int rank, Class<?> dtype) {
		int[] shape = new int[rank];
		Arrays.fill(shape, ILazyWriteableDataset.UNLIMITED);
		return initializeLazyDataset(name, shape, dtype);
	}
	
	@Override
	public ILazyWriteableDataset initializeLazyDataset(String name,
			int[] maxShape, Class<?> dtype) {
		ILazyWriteableDataset dataset = new LazyWriteableDataset(name, dtype, maxShape, null, null, null);
		createDataNode(name, dataset);
		
		return dataset;
	}
	
	@Override
	public ILazyWriteableDataset initializeFixedSizeLazyDataset(String name, int[] shape,
			Class<?> dtype) {
		ILazyWriteableDataset dataset = new LazyWriteableDataset(name, dtype, shape, shape, null, null);
		createDataNode(name, dataset);
		
		return dataset;
	}

	@Override
	public void addExternalLink(String name, String externalFileName, String pathToNode) {
		try {
			URI uri = new URI(externalFileName);
			SymbolicNode linkNode = NexusNodeFactory.createSymbolicNode(uri, pathToNode);
			addSymbolicNode(name, linkNode);
		} catch (URISyntaxException e) {
			// the filename is not a valid URI, not expected
			throw new IllegalArgumentException("Filename cannot be convert to a URI", e);
		}
	}

	@Override
	public DataNode createDataNode(String name, ILazyDataset value) {
		// note that this method should only be used when creating a new NeXus tree
		DataNode dataNode = NexusNodeFactory.createDataNode();
		addDataNode(name, dataNode);
		dataNode.setDataset(value);
		
		return dataNode;
	}

	@SuppressWarnings("unchecked")
	public <N extends NXobject> Map<String, N> getChildren(Class<N> nxClass) {
		final Map<String, N> map = new LinkedHashMap<>();
		for (NodeLink n : this) {
			if (n.isDestinationGroup()) {
				GroupNode g = (GroupNode) n.getDestination();
				if (g instanceof NXobject && ((NXobject) g).getNXclass().equals(nxClass)) {
					map.put(n.getName(), (N) g);
				}
			}
		}
		return map;
	}
	
	public Map<String, NXobject> getChildren() {
		final Map<String, NXobject> map = new LinkedHashMap<>();
		for (NodeLink n : this) {
			if (n.isDestinationGroup()) {
				GroupNode g = (GroupNode) n.getDestination();
				if (g instanceof NXobject) {
					map.put(n.getName(), (NXobject) g);
				}
			}
		}
		
		return map;
	}

	@Override
	public <N extends NXobject> void putChild(String name, N child) {
		if (containsGroupNode(name)) {
			throw new IllegalArgumentException("A group node already exists with the name " + name);
		}

		addGroupNode(name, child);
	}

	@Override
	public <N extends NXobject> void setChildren(Map<String, N> map) {
		map = new LinkedHashMap<>(map);
		for (String name : map.keySet()) {
			N child = map.get(name);
			addGroupNode(name, child);
		}
	}

	@Override
	public String getString(String name) {
		if (!containsDataNode(name)) {
			return null;
		}
		return getDataNode(name).getString();
	}

	public DataNode setString(String name, String value) {
		DataNode dataNode;
		if (containsDataNode(name)) {
			dataNode = getDataNode(name);
			if (!dataNode.isString()) {
				throw new IllegalArgumentException("Node is not a string");
			}
			dataNode.setString(value);
		} else {
			// create a new dataset, create a new DataNode containing that dataset
			StringDataset dataset = DatasetFactory.createFromObject(StringDataset.class, value);
			dataNode = createDataNode(name, dataset);
			// add the new dataset to the cache
			cached.put(name, dataset);
		}
		
		return dataNode;
	}

	@Override
	public Map<String, IDataset> getAllDatasets() {
		Map<String, IDataset> map = new LinkedHashMap<>();
		
		for (NodeLink n : this) {
			if (n.isDestinationData()) {
				map.put(n.getName(), getCached(n.getName()));
			}
		}
		return map;
	}

	@Override
	public Boolean getBoolean(String name) {
		Dataset d = getCached(name);
		return d == null ? null : d.getElementBooleanAbs(0);
	}

	@Override
	public Long getLong(String name) {
		Dataset d = getCached(name);
		return d == null ? null : d.getElementLongAbs(0);
	}

	@Override
	public Double getDouble(String name) {
		Dataset d = getCached(name);
		return d == null ? null : d.getElementDoubleAbs(0);
	}

	@Override
	public Number getNumber(String name) {
		Dataset d = getCached(name);
		if (d != null) {
			if (d.hasFloatingPointElements())
				return d.getElementDoubleAbs(0);
			return d.getElementLongAbs(0);
		}
		
		return null;
	}

	@Override
	public Date getDate(String name) {
		Dataset d = getCached(name);
		if (d instanceof DateDataset) {
			return ((DateDataset) d).getDate();
		}
		
		return null;
	}

	/**
	 * Set the value of the given field to the given value. The
	 * value may be an atomic value (e.g. primitive wrapper, object or string),
	 * or a dataset.
	 * @param name name
	 * @param value value
	 */
	@Override
	public DataNode setField(String name, Object value) {
		final DataNode dataNode;
		if (containsDataNode(name)) {
			dataNode = getDataNode(name);
			// create a new dataset, new DataNode and update the cache
			Dataset dataset = getCached(name);
			if (DTypeUtils.getDTypeFromObject(value) != dataset.getDType()) {
				throw new IllegalArgumentException("Cannot overwrite existing dataset of " + dataset.getElementClass());
			}
			
			dataset.setObjectAbs(0, value);
		} else {
			Dataset dataset = DatasetFactory.createFromObject(value);
			dataset.setName(name);
			dataNode = createDataNode(name, dataset);
			cached.put(name, dataset);
		}
		
		return dataNode;
	}

	protected DataNode setDate(String name, Date date) {
		return setField(name, date);
	}

	private static String makeAttributeKey(String name, String attrName) {
		return name == null ? ATTRIBUTE + attrName : name + ATTRIBUTE + attrName;
	}

	@Override
	public void setAttribute(String name, String attrName, Object attrValue) {
		Node node = name == null ? this : getNode(name);
		if (node == null) {
			throw new IllegalArgumentException("No group of field with name " + name);
		}
		
		Attribute a = node.containsAttribute(attrName) ? node.getAttribute(attrName) : TreeFactory.createAttribute(attrName);
		a.setValue(attrValue);
		node.addAttribute(a);
		Dataset d = DatasetUtils.convertToDataset(a.getValue());
		d.setName(attrName);
		cached.put(makeAttributeKey(name, attrName), d);
	}

	private Dataset getCachedAttribute(String name, String attrName) {
		String key = makeAttributeKey(name, attrName);
		if (!cached.containsKey(key)) {
			Node node = name == null ? this : getNode(name);
			if (node == null) {
				throw new IllegalArgumentException("No group of field with name " + name);
			}
			Attribute a = node.getAttribute(attrName);
			if (a != null) {
				cached.put(key, DatasetUtils.convertToDataset(a.getValue()));
			}
		}

		return cached.get(key);
	}
	
	@Override
	public Dataset getAttr(String name, String attrName) {
		return getCachedAttribute(name, attrName);
	}

	@Override
	public String getAttrString(String name, String attrName) {
		Node node = name == null ? this : getNode(name);
		Attribute a = node.getAttribute(attrName);
		return a == null ? null : a.getFirstElement();
	}

	@Override
	public Boolean getAttrBoolean(String name, String attrName) {
		Dataset d = getCachedAttribute(name, attrName);
		return d == null ? null : d.getElementBooleanAbs(0);
	}

	@Override
	public Long getAttrLong(String name, String attrName) {
		Dataset d = getCachedAttribute(name, attrName);
		return d == null ? null : d.getElementLongAbs(0);
	}

	@Override
	public Double getAttrDouble(String name, String attrName) {
		Dataset d = getCachedAttribute(name, attrName);
		return d == null ? null : d.getElementDoubleAbs(0);
	}

	@Override
	public Number getAttrNumber(String name, String attrName) {
		Dataset d = getCachedAttribute(name, attrName);
		if (d == null) {
			return null;
		}
		if (d.hasFloatingPointElements()) {
			return d.getElementDoubleAbs(0);
		}
		
		return d.getElementLongAbs(0);
	}

	@Override
	public Date getAttrDate(String name, String attrName) {
//		try {
//			return DateFormat.getDateTimeInstance().parse(getAttrString(name, attrName));
//		} catch (ParseException e) {
//			return null;
//		}
		Dataset d = getCachedAttribute(name, attrName);
		if (d instanceof DateDataset) {
			return ((DateDataset) d).getDate();
		}
		
		return null;
	}
	
	@Override
	protected void appendNodeString(StringBuilder s, String n) {
		s.append(INDENT);
		s.append(n);
		Node node = getNode(n);
		if (node instanceof SymbolicNode)
			s.append('@');
		else if (node instanceof GroupNode) {
			if (node instanceof NXobject) { // append type of NXobject
				s.append('[');
				s.append(((NXobject) node).getNexusBaseClass());
				s.append(']');
			}
			s.append('/');
		}
//			else
//				s.append(String.format("(%d)", node.getID()));
		s.append('\n');
	}

}
